July 2010
M T W T F S S
« Dec    
 1234
567891011
12131415161718
19202122232425
262728293031  

Pages

  • 29Dec

    While I am thoroughly enjoying MVC and autofac, I feel there is something missing when it comes to handling areas. I found areas an excellent idea for segregating larger web apps into manageable chunks when using MonoRail a few years ago. I was unsure of how to incorporate them into MVC until I found Phil Haack’s blog entryand subsequent followup by Steve Sanderson.

    This made for a great start. One large problem: controller names had to be unique across every area. At first I resigned myself to having *really* long class names. But then I came to my senses and started hunting through the code for autofac to figure out how to change this. After a few hours of trial and error I figured out what changes needed to be made.

    Create a new AreaAwareControllerFactory that extends the built-in factory:

        public class AreaAwareControllerFactory : AutofacControllerFactory
        {
            public AreaAwareControllerFactory(IContainerProvider containerProvider)
                : base(containerProvider)
            {
            }
    
            public AreaAwareControllerFactory(IContainerProvider containerProvider,
                                              IControllerIdentificationStrategy controllerIdentificationStrategy)
                : base(containerProvider, controllerIdentificationStrategy)
            {
            }
    
            public override IController CreateController(RequestContext context, string controllerName)
            {
                var area = (string)context.RouteData.Values["area"];
    
                if (area != null)
                {
                    controllerName = area + "." + controllerName;
                }
    
                return base.CreateController(context, controllerName);
            }
        }

    This version relies on a custom attribute to specify an area and it must match the area routing key:

        [AttributeUsage(AttributeTargets.Class)]
        public class AreaAttribute : Attribute
        {
            public string Name { get; set; }
    
            public AreaAttribute(string name)
            {
                Name = name;
            }
        }
    

    The next step is to create an area aware identification strategy:

        public class AreaAwareControllerIdentificationStrategy : IControllerIdentificationStrategy
        {
            private const string Prefix = "controller.";
            private const string TypeNameSuffix = "Controller";
    
            public Service ServiceForControllerName(string controllerName)
            {
                if (controllerName == null)
                {
                    throw new ArgumentNullException("controllerName");
                }
    
                if (controllerName == "")
                {
                    throw new ArgumentException("controllerName");
                }
    
                return new NamedService(Prefix + controllerName.ToLowerInvariant());
            }
    
            public Service ServiceForControllerType(Type controllerType)
            {
                // see if controller has Area attribute
                var attr = (from attribute in controllerType.GetCustomAttributes(typeof (AreaAttribute), true)
                            select attribute).Cast().FirstOrDefault();
    
                if (attr != null)
                {
                    return ServiceForControllerName(attr.Name + "." + controllerType.Name.Replace(TypeNameSuffix, ""));
                }
    
                return ServiceForControllerName(controllerType.Name.Replace(TypeNameSuffix, ""));
            }
        }

    The final step is to wire up the module and factory:

    private void RegisterContainer()
    {
        var builder = new ContainerBuilder();
    
        // register core modules
        builder.RegisterModule(new AutofacControllerModule(controllersAssembly)
           {
               IdentificationStrategy = new AreaAwareControllerIdentificationStrategy()
           });
    
        containerProvider = new ContainerProvider(builder.Build());
    
        ControllerBuilder.Current.SetControllerFactory(new AreaAwareControllerFactory(ContainerProvider));
    }
    

    I also made a minor change to where views are located. Replace the original functions in AreaViewEngine with the following:

        public class AreaAwareViewEngine : WebFormViewEngine
        {
            private static string _appBase;
    
            public AreaAwareViewEngine(string appBase)
            {
                _appBase = appBase.Trim('/', '~');
            }
    
            public AreaAwareViewEngine()
                : base()
            {
                _appBase = "~";
    
                ViewLocationFormats = new[] {
                                                _appBase + "/{0}.aspx",
                                                _appBase + "/{0}.ascx",
                                                _appBase + "/Views/{1}/{0}.aspx",
                                                _appBase + "/Views/{1}/{0}.ascx",
                                                _appBase + "/Views/Shared/{0}.aspx",
                                                _appBase + "/Views/Shared/{0}.ascx",
                                            };
    
                MasterLocationFormats = new[] {
                                                  _appBase + "/{0}.master",
                                                  _appBase + "/Shared/{0}.master",
                                                  _appBase + "/Views/{1}/{0}.master",
                                                  _appBase + "/Views/Shared/{0}.master",
                                              };
    
                PartialViewLocationFormats = ViewLocationFormats;
            }
    
            public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
            {
                ViewEngineResult areaResult = null;
    
                if (controllerContext.RouteData.Values.ContainsKey("area"))
                {
                    string areaPartialName = FormatViewName(controllerContext, partialViewName);
                    areaResult = base.FindPartialView(controllerContext, areaPartialName, useCache);
                    if (areaResult != null && areaResult.View != null)
                    {
                        return areaResult;
                    }
    
                    string sharedAreaPartialName = FormatAreaSharedViewName(controllerContext, partialViewName);
                    areaResult = base.FindPartialView(controllerContext, sharedAreaPartialName, useCache);
                    if (areaResult != null && areaResult.View != null)
                    {
                        return areaResult;
                    }
    
                    sharedAreaPartialName = FormatSharedViewName(controllerContext, partialViewName);
                    areaResult = base.FindPartialView(controllerContext, sharedAreaPartialName, useCache);
                    if (areaResult != null && areaResult.View != null)
                    {
                        return areaResult;
                    }
                }
    
                return base.FindPartialView(controllerContext, partialViewName, useCache);
            }
    
            public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
            {
                ViewEngineResult areaResult = null;
    
                if (controllerContext.RouteData.Values.ContainsKey("area"))
                {
                    string areaViewName = FormatViewName(controllerContext, viewName);
                    areaResult = base.FindView(controllerContext, areaViewName, masterName, useCache);
                    if (areaResult != null && areaResult.View != null)
                    {
                        return areaResult;
                    }
    
                    string sharedAreaViewName = FormatSharedViewName(controllerContext, viewName);
                    areaResult = base.FindView(controllerContext, sharedAreaViewName, masterName, useCache);
                    if (areaResult != null && areaResult.View != null)
                    {
                        return areaResult;
                    }
                }
    
                return base.FindView(controllerContext, viewName, masterName, useCache);
            }
    
            private static string FormatViewName(ControllerContext controllerContext, string viewName)
            {
                string controllerName = controllerContext.RouteData.GetRequiredString("controller");
    
                string area = controllerContext.RouteData.Values["area"].ToString();
                return "Views/" + area + "/" + controllerName + "/" + viewName;
            }
    
            private static string FormatSharedViewName(ControllerContext controllerContext, string viewName)
            {
                string area = controllerContext.RouteData.Values["area"].ToString();
                return "Views/Shared/" + area + "/" + viewName;
            }
    
            private static string FormatAreaSharedViewName(ControllerContext controllerContext, string viewName)
            {
                string area = controllerContext.RouteData.Values["area"].ToString();
                return "Views/" + area + "/Shared/" + viewName;
            }
    
        }
    

    You may be wondering why I don’t pass in the new identifying strategy to the factory and that’s because of a limitation in the way IControllerIdentificationStrategy works. This interface doesn’t allow you to consider RouteData when generating the service names. This leave us with having to subclass the factory. In the future it would be nice to see a more configurable way to discover controller names.

    ASP.NET MVC and autofac amaze me at every turn with their extensibility.

    Posted by mthird @ 5:59 pm

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.